<?php
/**    
  *
  * Copyright (c) 2009, Persistent Systems Limited
  *
  * Redistribution and use, with or without modification, are permitted 
  *  provided that the following  conditions are met:
  *   - Redistributions of source code must retain the above copyright notice, 
  *     this list of conditions and the following disclaimer.
  *   - Neither the name of Persistent Systems Limited nor the names of its contributors 
  *     may be used to endorse or promote products derived from this software 
  *     without specific prior written permission.
  *
  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 
  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 
  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 
  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 
  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 
  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 
  * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
  * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 
  * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 
  * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
  
/**
  *
  * This file contains the definition of ObjectContext class.
  */

class ObjectContext
{
    protected $_baseURI;
    protected $_entities = array();
    protected $_entitySet2Type = array();
    protected $_entityType2Set = array();
              
    /**
     * This dictioary will be used to track all the data service entites
     * currently in the context.
     * Key=>Object : Value=>ResourceBox
     */ 
    public $ObjectToResource;
    /**
     * This dictioary will be used to track all the related entites
     * currently in the context.
     * Key=>RelatedEnd : Value=>RelatedEnd
     */ 
    public $Bindings;
    
    /**
     * This array will track the objects in the context
     * which have identity (ex: object in the context which
     * result of a query execution)
     */
    public $IdentityToResource;
    
    /**
     * Hold the id of next chnaged entries
     */
    protected $_nextChange;
    
    /**
     * Credential Object holding credential information
     */ 
    public $Credential;  
    
    /**
     * Proxy Object holding ProxyServer information
     */ 
    public $HttpProxy;    
    
    /**
     * This array holds custom headers as key-value
     */
    public $CustomHeaders = array();

    public $Accept;
    public $Content_Type;
    
    public function __construct()
    {
    }
    
    /** 
     * Constructs a new ObjectContext object.
     * @param string $uri The Base uri.
     */
    public function ObjectContext($uri) 
    {
        $this->ObjectToResource = new Dictionary();
        $this->Bindings = new Dictionary();
        $this->IdentityToResource = array();
        $this->_nextChange = 0;
        $this->Credential = null;
        $this->HttpProxy = null;
        $this->_baseURI = $uri;
        $this->_baseURIWithSlash = $this->_baseURI;
        $this->CustomHeaders = array();
        if($this->_baseURIWithSlash[(strlen($this->_baseURIWithSlash) - 1)] != '/')
        {
            $this->_baseURIWithSlash = $this->_baseURIWithSlash . '/';
        }
        
        
        $this->Accept = Resource::Accept_JSON;
        $this->Content_Type = Resource::Content_Type_JSON;
        
        //$this->Accept = Resource::Accept_ATOM;
        //$this->Content_Type = Resource::Content_Type_ATOM;
        
        
    }
    
    /** 
     * To insert an object into data service.
     * @param string $entityName The class name of entity to be inserted.
     * @param $object The instance of entity to be inserted.
     */
    public function AddObject($entityName, $object)
    {	
        $this->ThrowExceptionIfNotValidObject($object, "AddObject");
        if ($this->ObjectToResource->ContainsKey($object))
        {
            throw new ADODotNETDataServicesException("Invalid Operation: " . Resource::EntityAlreadyContained);
        }
        $editLink = $entityName;
        $resourceBox = new ResourceBox(null, $editLink, $object);
        $resourceBox->State = EntityStates::Added;
        $this->ObjectToResource->Add($object, $resourceBox);
        $this->IncrementChange($resourceBox);  
    }
    
    /** 
     * To update an entity instance in data service.
     * @param $object The instance of entity to be updated.
     */
    public function UpdateObject($object)
    {
        $this->ThrowExceptionIfNotValidObject($object, "UpdateObject");
        $resourcebox = null;
        if (!$this->ObjectToResource->TryGetValue($object, $resourcebox))
        {
            throw new ADODotNETDataServicesException("Invalid Operation:" . Resource::EntityNotContained);
        } 
        if (EntityStates::Unchanged == $resourcebox->State)
        {
            $resourcebox->State = EntityStates::Modified;
            $this->IncrementChange($resourcebox);
        }
    }
    
    /** 
     * To create an association between two entity instances
     * @param sourceObject The source object participating the association.
     * @param targetObject The target object participating the association.
     * @param entityName The class name of target object.
     */
    public function AddLink($sourceObject, $sourceProperty, $targetObject)
    {
        $this->ThrowExceptionIfNotValidObject($sourceObject, "AddLink");
        $this->ThrowExceptionIfNotValidObject($targetObject, "AddLink");
        $key = new RelatedEnd($sourceObject, $sourceProperty, $targetObject);
        $this->ValidateAddLink($key);
        $key->State = EntityStates::Added;
        $this->Bindings->Add($key, $key);
        $sourceResourceBox = null;
        $this->ObjectToResource->TryGetValue($sourceObject, $sourceResourceBox);
        $sourceResourceBox->RelatedLinkCount++;
        $this->IncrementChange($key);
    }
    
    /** 
     * To delete an entity instance from the data service.
     * @param object The entity instance to be deleted.     
     */
    public function DeleteObject($object)
    {		
        $this->ThrowExceptionIfNotValidObject($object, "DeleteObject");
        $resourceBox = null;
        if (!$this->ObjectToResource->TryGetValue($object, $resourceBox))
        {
            throw new ADODotNETDataServicesException("Invalid Operation:" . Resource::EntityNotContained);
        }
        $state = $resourceBox->State;
        if (EntityStates::Added == $state)
        {		  
            if (null != $resourceBox->Identity)
            {
                unset($this->IdentityToResource[$resourceBox->Identity]);
            }
            $this->DetachRelated($resourceBox);
            $resourceBox->State = EntityStates::Detached;
            $this->ObjectToResource->Remove($object);
        }
        else if (EntityStates::Deleted != $state)
        {
            $resourceBox->State = EntityStates::Deleted;
            $this->IncrementChange($resourceBox);
        }		
    }

    /** 
     * To delete an association between two entity instances
     * @param <Object> sourceObject The source object participating the association.
     * @param <Object> targetObject The target object participating the association.
     * @param <string> entityName The class name of target object.
     * @Return No return value
     */
    public function DeleteLink($sourceObject, $sourceProperty, $targetObject)
    {
        $this->ThrowExceptionIfNotValidObject($sourceObject, "DeleteLink");
        $this->ThrowExceptionIfNotValidObject($targetObject, "DeleteLink");
        $key = new RelatedEnd($sourceObject, $sourceProperty, $targetObject);
        $this->ValidateDeleteLink($key);
        $bindingValue = null;
        $this->Bindings->TryGetValue($key, $bindingValue);
        if($bindingValue != null && EntityStates::Added == $bindingValue->State)
        {
            $this->DetachExistingLink($key);
            return;
        }
        $sourceResourceBox = null;
        $this->ObjectToResource->TryGetValue($sourceObject, $sourceResourceBox);
        $targetResourcebox = null;
        $this->ObjectToResource->TryGetValue($targetObject, $targetResourcebox);
        
        if((($bindingValue == null) && 
                 ((EntityStates::Added == $sourceResourceBox->State) ||
                  (EntityStates::Added == $targetResourcebox->State))))
        {
            throw new ADODotNETDataServicesException("Invalid Operation:" . Resource::NoRelationWithInsertEnd);
        }
        else if ($bindingValue == null)
        {
            $this->Bindings->Add($key, $key);
            $sourceResourceBox->RelatedLinkCount++;
            $bindingValue = $key;
        }
        if (EntityStates::Deleted != $bindingValue->State)
        {
            $bindingValue->State = EntityStates::Deleted;
            $this->IncrementChange($bindingValue);
        }
    }
    
    /**
     * This function will updates the entities changed entites (added, modifed, deleted)
     * in the store
     * @Return No return value
     */
    public function SaveChanges()
    {
        $result = new SaveResult($this);
        $result->BatchRequest();
    }
    
    /**
     * @Return <bool> 
     * return true of entry (ResourceBox, RelatedEnd) has modifed else false     
     */
    public function HasModifiedResourceState($entry)
    {
        if(EntityStates::Unchanged != $entry->State)
        {
            return TRUE;
        }
        return FALSE;
    }
    
    /** 
     * @param <Object> $SourceObject Instance of the entity into which value of the property to be loaded.
     * @param <Object> $PropertyName Name of the property whose value to be loaded.
     * @Return No return value
     * Load the value of the specified property from data service
     */
    public function LoadProperty($SourceObject, $PropertyName) 
    {
        $query = Utility::getUri($SourceObject)
               . "/" 
               . $PropertyName;
        $entities = $this->ExecuteQuery($query);
        $prop = new ReflectionProperty($SourceObject, $PropertyName);
        $prop->setValue($SourceObject, $entities);
    }
    
    /** 
     * @param <string> $query Query to be executed.
     * @return <Array of Objects> Result of query execution as a collection of entities. 
     * For executing a query in data serivice.     
     */
    public function ExecuteQuery($query) 
    {
        try 
        {
            $Queryobj = new QueryProcessor($query);
            $query = $Queryobj->get_encodedquery();
            return $this->getQueryEntities($this->_baseURI . $query);
        } 
        catch (ADODotNETDataServicesException $e) 
        {
            throw $e;
        } 
        catch (Exception $e) 
        {
            throw new ADODotNETDataServicesException($e->getMessage());
        }
    }
    
    /**
     * Increment the changeOrder associated with Entry (ResourceBox or RelatedEnd)
     * @Return No return value
     */
    protected function IncrementChange($resourceBoxOrRelatedEnd)
    {
            $resourceBoxOrRelatedEnd->ChangeOrder = ++$this->nextChange;
    }
    
    /** 
     * @param <RelatedEnd> $relatedEnd
     * @Return No return value
     * Check whether creating a link between $relatedEnd::SourceResource and 
     * $relatedEnd::TargetResource is valid based on current states
     */
    protected function ValidateAddLink($relatedEnd)
      {
            $sourceObject = $relatedEnd->GetSourceResource();
            $sourceProperty = $relatedEnd->GetSourceProperty();
            $targetObject = $relatedEnd->GetTargetResource();
            $sourceResourceBox = null;
            if (!$this->ObjectToResource->TryGetValue($sourceObject, $sourceResourceBox))
            {
                  throw new ADODotNETDataServicesException("Invalid Operation:" . Resource::EntityNotContained);
            }
            $targetResourceBox = null;
            if (!$this->ObjectToResource->TryGetValue($targetObject, $targetResourceBox))
            {
                throw new ADODotNETDataServicesException("Invalid Operation:" . Resource::EntityNotContained);
            }
            if (($sourceResourceBox->State == EntityStates::Deleted) ||
                (($targetResourceBox != null) && ($targetResourceBox->State == EntityStates::Deleted)))
            {
                throw new ADODotNETDataServicesException("Invalid Operation:" . Resource::NoRelationWithDeleteEnd);
            }
            if ($this->Bindings->ContainsKey($relatedEnd))
            {
                throw new ADODotNETDataServicesException("Invalid Operation:" . Resource::RelationAlreadyContained);
            }
      }
    
    /** 
     * @param <RelatedEnd> $relatedEnd
     * @Return No return value
     * Check whether deleteing a link between $relatedEnd::SourceResource and 
     * $relatedEnd::TargetResource is valid based on current states
     */
    protected function ValidateDeleteLink($relatedEnd)
    {
        $sourceObject = $relatedEnd->GetSourceResource();
        $sourceProperty = $relatedEnd->GetSourceProperty();
        $targetObject = $relatedEnd->GetTargetResource();
        $sourceResourceBox = null;
        if (!$this->ObjectToResource->TryGetValue($sourceObject, $sourceResourceBox))
        {
            throw new ADODotNETDataServicesException("Invalid Operation:" . Resource::EntityNotContained);
        }
        $targetResourceBox = null;
        if (!$this->ObjectToResource->TryGetValue($targetObject, $targetResourceBox))
        {
            throw new ADODotNETDataServicesException("Invalid Operation:" . Resource::EntityNotContained);
        }
        
    }
    
    /**
     * @param <ResourceBox> $resourceBox
     * @Return No return value
     * Detach all bindings created with $resourceBox::Resource as 
     * source object
     */
    protected function DetachRelated($resourceBox)
    {
        $bindingValues = $this->Bindings->Values();
        foreach($bindingValues as $bindingValue)
        {		
            if($resourceBox->IsRelatedEntity($bindingValue))
            {				
                $this->DetachExistingLink($bindingValue);
            }
        }
    }
    
    /**
     * @param <RelatedEnd> $relatedEnd
     * Remove the $relatedEnd from Binding dictionary
     */
    protected function DetachExistingLink($relatedEnd)
    {
        if ($this->Bindings->Remove($relatedEnd))
        {
            $relatedEnd->State = EntityStates::Detached;
            $resourceBox = null;
            $this->ObjectToResource->TryGetValue($relatedEnd->getSourceResource(), $resourceBox);
            $resourceBox->RelatedLinkCount--;
        }
    }
    
    /**
     * @param <array> $phpNative
     * @param<ResourceBox> $resourceBox
     * This function will update the $resourceBox::Source object with json data in
     * $phpNative array
     */
    public function LoadResourceBox($str, $resourceBox, $content_type)
    {
        $resource = $resourceBox->GetResource();
        $uri = "";
        Parser::PopulateObject($str, $resource, $uri, $content_type);
        if(isset($uri))
        {
            $index = Utility::reverseFind($uri, '/');
            $editLink = substr($uri,$index + 1, strlen($uri) - $index);
            $resourceBox->Identity = $uri;
            $resourceBox->EditLink = $editLink;
        }
        
    }	
    
    /**
     * @param <string> $entityType Name of entity Type
     * @param <string> $uri Identity of the entity instance in data service
     * @return <Object> The object representing the entity instance
     * This function will check whether an object with identity $uri 
     * exists in the context, if it exists return that object. If not
     * create the object for that entity instance, add it to ObjectToResource
     * and return it.
     */
    public function AddToObjectToResource($entityType, $uri)
    {
        if( array_key_exists($uri, $this->IdentityToResource))
        {
            return $this->IdentityToResource[$uri]->getResource();
        }
        
        try
        { 
            $class = new ReflectionClass($entityType);
            $resource = $class->newInstance($uri);
            $index = Utility::reverseFind($uri, '/');
            $editLink = substr($uri,$index + 1, strlen($uri) - $index);
            $resourceBox = new Resourcebox($uri, $editLink, $resource);
            $resourceBox->State = EntityStates::Unchanged;
            $this->ObjectToResource->Add($resource, $resourceBox);
            $this->IdentityToResource[$uri] = $resourceBox;

            return $resource;
        }
        catch (ReflectionException $ex)
        {
            throw new ADODotNETDataServicesException("Invalid entity set name: " . $entityName);
        }
    }
    
    /**
     * @param <Object> $sourceObject
     * @param <string> $sourcePropertyName
     * @param <Object> $object
     * When user perfroms a query operation with $expand option or LoadProperty
     * method, then the binding between the SourceObject and Target object become 
     * binding in the context. This function will add such a binding to Bindinds
     * dictionary
     */
    public function AddToBindings($sourceObject, $sourcePropertyName, $object)
    {
        $binding = new RelatedEnd($sourceObject, $sourcePropertyName, $object);
        if ($this->Bindings->ContainsKey($binding) == FALSE)
        {
            $binding->State = EntityStates::Unchanged;
            $this->Bindings->Add($binding, $binding);
        }
    }
    
    /**
     * @param <Object> $object
     * @param <string> $from
     *	Test the object $object is an object if not throws exception
     */
    protected function ThrowExceptionIfNotValidObject($object, $from)
    {
        if(is_object($object))
        {
            return;
        }
        $message;
        switch($from)
        {
            case "AddObject":
                $message = "Trying to Add Invalid Object";
                break;
            case "UpdateObject":
                $message = "Trying to Update Invalid Object";
                break;
            case "DeleteObject":
                $message = "Trying to Delete Invalid Object";
                break;
            case "AddLink":
                $message = "Trying to Add Link between Objects, where one or both objects are invalid";
                break;
            case "DeleteLink":
                $message = "Trying to Delete Link between Objects, where one or both objects are invalid";
                break;
            default:
                $message = "Object is not valid";
        }
        throw new ADODotNETDataServicesException($message);
    }
    
    /**
     * Wrapper function over getHttpResponse and
     * getEntityResult
     * @param string $uri The uri point to the entities
     * to be retrived.
     * @return Array Entity Set corrosponding to $uri.
     */
    public function getQueryEntities($uri)
    {
        
        $standardHeaders = array(
            "Accept" => $this->Accept, 
            "Content-Type" => $this->Content_Type, 
            "DataServiceVersion" => Resource::DataServiceVersion, 
            "MaxDataServiceVersion" => Resource::MaxDataServiceVersion
        ); 
        $mergedHeaders = array_merge($standardHeaders, $this->CustomHeaders);
        $httpRequest = new HttpRequest('GET', $uri, $this->Credential, $this->HttpProxy, $mergedHeaders);
        $httpRawResponse = $httpRequest->GetResponse();   
        $httpResponse = HttpResponse::fromString($httpRawResponse);  
        $headers = $httpResponse->getHeaders();
        $content_type = isset($headers['Content-type']) ? $headers['Content-type'] : "";
        
        //Check for IIS Error (ex: Unauthorized)
        //Also check the content type if it json or atom format pass it to corrosponding
        //Parser.
       
        if($httpResponse->IsError())
        {         
            if ((strpos(strtolower($content_type), strtolower(Resource::Content_Type_JSON)) === FALSE))
            {
                throw new ADODotNETDataServicesException($httpResponse->getBody(), $content_type);
            }
            else if((strpos(strtolower($content_type), strtolower(Resource::Content_Type_ATOM)) === FALSE))
            {
                throw new ADODotNETDataServicesException($httpResponse->getBody(), $content_type);
            }
            else
            {
                throw new ADODotNETDataServicesException("Http Message: " . $httpResponse->getMessage() . 
                                                     " Http Code: " . $httpResponse->getCode());
            }
        }
        
        //Check for version mismatch        
        if(isset($headers['Dataserviceversion']) && 
        ((int)$headers['Dataserviceversion'] > (int)Resource::DataServiceVersion))
        {
                throw new ADODotNETDataServicesException(Resource::VersionMisMatch . $headers['Dataserviceversion']);
        }
        
        $parser = new Parser();
        return  $parser->EnumerateObjects($httpResponse->getBody(), $this, $content_type);
    }
    
    /**
     * For adding a custom header.
     * @param string $headerName The custom header name
     * @param string $HeaderValue The custom header value
     */
    public function addHeader($headerName, $HeaderValue)
    {
        $this->CustomHeaders[$headerName] = $HeaderValue;
    }
    
    /**
     * For clearing the array holding custom headers.
     */
    public function removeHeaders()
    {
        unset($this->CustomHeaders);
    }
    
    /**
     * @Return <string> 
     * Returns data service uri with slash
     */ 
    public function GetBaseUri()
    {
        return $this->_baseURI;
    }
    
    /**
     * @Return <string> 
     * Returns data service base uri with slash
     */
    public function GetBaseUriWithSlash()
    {
        return $this->_baseURIWithSlash;
    }
    
    /**
     * Tis function returns the entity set name corrosponding to entity type
     * @param <string> $entityType The Entity Type
     * @Return <string> 
     * Returns Entity set Name
     */
    public function GetEntitySetNameFromType($entityType)
    {
        $entitySet = $this->_entityType2Set[strtolower($entityType)];
        if (empty($entitySet))
        {
            throw new ADODotNETDataServicesException("Invalid entity type: " . $entityType);
        }
        return $entitySet;
    }
    
    public function GetEntityTypeNameFromSet($entitySet)
    {
        $entityType = $this->_entitySet2Type[strtolower($entitySet)];
        if (empty($entityType))
        {
            throw new ADODotNETDataServicesException("Invalid entity Set: " . $entitySet);
        }
        return $entityType;
    }
}

?>